# импортируем библиотеки
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
# настроим вывод графиков
import plotly.io as pio
pio.renderers.default='notebook'
hh_data = pd.read_csv('dst-3.0_16_1_hh_database.csv', sep=';')
hh_data.head(5)
| Пол, возраст | ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Образование и ВУЗ | Обновление резюме | Авто | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Мужчина , 39 лет , родился 27 ноября 1979 | 29000 руб. | Системный администратор | Советск (Калининградская область) , не готов к... | частичная занятость, проектная работа, полная ... | гибкий график, полный день, сменный график, ва... | Опыт работы 16 лет 10 месяцев Август 2010 — п... | МАОУ "СОШ № 1 г.Немана" | Системный администратор | Неоконченное высшее образование 2000 Балтийск... | 16.04.2019 15:59 | Имеется собственный автомобиль |
| 1 | Мужчина , 60 лет , родился 20 марта 1959 | 40000 руб. | Технический писатель | Королев , не готов к переезду , готов к редким... | частичная занятость, проектная работа, полная ... | гибкий график, полный день, сменный график, уд... | Опыт работы 19 лет 5 месяцев Январь 2000 — по... | Временный трудовой коллектив | Менеджер проекта, Аналитик, Технический писатель | Высшее образование 1981 Военно-космическая ак... | 12.04.2019 08:42 | Не указано |
| 2 | Женщина , 36 лет , родилась 12 августа 1982 | 20000 руб. | Оператор | Тверь , не готова к переезду , не готова к ком... | полная занятость | полный день | Опыт работы 10 лет 3 месяца Октябрь 2004 — Де... | ПАО Сбербанк | Кассир-операционист | Среднее специальное образование 2002 Профессио... | 16.04.2019 08:35 | Не указано |
| 3 | Мужчина , 38 лет , родился 25 июня 1980 | 100000 руб. | Веб-разработчик (HTML / CSS / JS / PHP / базы ... | Саратов , не готов к переезду , готов к редким... | частичная занятость, проектная работа, полная ... | гибкий график, удаленная работа | Опыт работы 18 лет 9 месяцев Август 2017 — Ап... | OpenSoft | Инженер-программист | Высшее образование 2002 Саратовский государст... | 08.04.2019 14:23 | Не указано |
| 4 | Женщина , 26 лет , родилась 3 марта 1993 | 140000 руб. | Региональный менеджер по продажам | Москва , не готова к переезду , готова к коман... | полная занятость | полный день | Опыт работы 5 лет 7 месяцев Региональный мене... | Мармелад | Менеджер по продажам | Высшее образование 2015 Кгу Психологии и педаг... | 22.04.2019 10:32 | Не указано |
hh_data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 44744 entries, 0 to 44743 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Пол, возраст 44744 non-null object 1 ЗП 44744 non-null object 2 Ищет работу на должность: 44744 non-null object 3 Город, переезд, командировки 44744 non-null object 4 Занятость 44744 non-null object 5 График 44744 non-null object 6 Опыт работы 44576 non-null object 7 Последнее/нынешнее место работы 44743 non-null object 8 Последняя/нынешняя должность 44742 non-null object 9 Образование и ВУЗ 44744 non-null object 10 Обновление резюме 44744 non-null object 11 Авто 44744 non-null object dtypes: object(12) memory usage: 4.1+ MB
hh_data.isnull().sum()
Пол, возраст 0 ЗП 0 Ищет работу на должность: 0 Город, переезд, командировки 0 Занятость 0 График 0 Опыт работы 168 Последнее/нынешнее место работы 1 Последняя/нынешняя должность 2 Образование и ВУЗ 0 Обновление резюме 0 Авто 0 dtype: int64
hh_data.describe(include='object')
| Пол, возраст | ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Образование и ВУЗ | Обновление резюме | Авто | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44576 | 44743 | 44742 | 44744 | 44744 | 44744 |
| unique | 16003 | 690 | 14929 | 10063 | 38 | 47 | 44413 | 30214 | 16927 | 40148 | 18838 | 2 |
| top | Мужчина , 32 года , родился 17 сентября 1986 | 50000 руб. | Системный администратор | Москва , не готов к переезду , не готов к кома... | полная занятость | полный день | Опыт работы 10 лет 8 месяцев Апрель 2018 — по... | Индивидуальное предпринимательство / частная п... | Системный администратор | Высшее образование 1987 Военный инженерный Кра... | 07.05.2019 09:50 | Не указано |
| freq | 18 | 4064 | 3099 | 1261 | 30026 | 22727 | 3 | 935 | 2062 | 4 | 25 | 32268 |
hh_data.shape
(44744, 12)
def education(x):
x = x.split()
if x[1] == 'образование':
return ' '.join(x[:1])
return ' '.join(x[:2])
hh_data['Образование'] = hh_data['Образование и ВУЗ'].apply(education)
hh_data['Образование'] = hh_data['Образование'].astype('category')
Удалим признак 'Образование и ВУЗ'
hh_data = hh_data.drop(['Образование и ВУЗ'], axis=1)
Сколько соискателей имеет средний уровень образования (школьное образование)?
hh_data['Образование'].value_counts()
Высшее 33863 Среднее специальное 5765 Неоконченное высшее 4557 Среднее 559 Name: Образование, dtype: int64
def gender(x):
x = x.replace(' ', ' ')
x = x.split(' , ')
return str(x[0][0])
def age(x):
x = x.replace(' ', ' ')
return int(x.split(' , ')[1].split(' ')[0])
hh_data['Пол'] = hh_data['Пол, возраст'].apply(gender)
hh_data['Возраст'] = hh_data['Пол, возраст'].apply(age)
Удалим признак «Пол, возраст» из таблицы.
hh_data = hh_data.drop(['Пол, возраст'], axis=1)
Сколько процентов женских резюме представлено в наших данных?
round(len(hh_data[hh_data['Пол'] == 'Ж']) / len(hh_data) * 100, 2)
19.07
hh_data.describe(include='object')
| ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Обновление резюме | Авто | Пол | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 44744 | 44744 | 44744 | 44744 | 44744 | 44576 | 44743 | 44742 | 44744 | 44744 | 44744 |
| unique | 690 | 14929 | 10063 | 38 | 47 | 44413 | 30214 | 16927 | 18838 | 2 | 2 |
| top | 50000 руб. | Системный администратор | Москва , не готов к переезду , не готов к кома... | полная занятость | полный день | Опыт работы 10 лет 8 месяцев Апрель 2018 — по... | Индивидуальное предпринимательство / частная п... | Системный администратор | 07.05.2019 09:50 | Не указано | М |
| freq | 4064 | 3099 | 1261 | 30026 | 22727 | 3 | 935 | 2062 | 25 | 32268 | 36211 |
Чему равен средний возраст соискателей?
round(hh_data['Возраст'].mean(), 1)
32.2
выделим общий опыт работы соискателя в месяцах, новый признак назовем "Опыт работы (месяц)"
def get_experience(arg):
if arg is np.nan or arg == 'Не указано':
return None
year_words=['год', 'года', 'лет']
month_words=['месяц', 'месяца', 'месяцев']
arg_splitted = arg.split(' ')[:7]
years = 0
months = 0
for index, item in enumerate (arg_splitted):
if item in year_words:
years = int(arg_splitted[index-1])
if item in month_words:
months = int(arg_splitted[index-1])
return int(years*12 + months)
hh_data['Опыт работы (месяц)'] = hh_data['Опыт работы'].apply(get_experience)
Медианный опыт работы (в месяцах)?
hh_data['Опыт работы (месяц)'].median()
100.0
Удалим столбец «Опыт работы» из таблицы.
hh_data = hh_data.drop(['Опыт работы'], axis=1)
Признак "Город" должен содержать только 4 категории: "Москва", "Санкт-Петербург" и "город-миллионник", остальные обозначим как "другие".
Признак "Готовность к переезду" должен иметь два возможных варианта: True или False.
Признак "Готовность к командировкам" должен иметь два возможных варианта: True или False.
Город:
def city_f(x):
million_cities = ['Новосибирск', 'Екатеринбург','Нижний Новгород','Казань', 'Челябинск','Омск', 'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж','Волгоград']
x = x.replace(' ', ' ').split(' , ')[0]
if x == 'Москва':
return 'Москва'
if x == 'Санкт-Петербург':
return 'Санкт-Петербург'
if x in million_cities:
return 'город-миллионник'
return 'другие'
hh_data['Город'] = hh_data['Город, переезд, командировки'].apply(city_f)
hh_data['Город'] = hh_data['Город'].astype('category')
Готовность к переезду:
def removal_f(x):
x = x.replace(' ', ' ').split(' , ')
if x[1][0] == 'м':
x = x[2]
else:
x = x[1]
x = x.split(' ')
for i in x:
if i == 'не':
return False
return True
hh_data['Готовность к переезду'] = hh_data['Город, переезд, командировки'].apply(removal_f)
Готовность к командировкам:
def business_trips_f(x):
if x is np.nan:
return False
x = x.replace(' ', ' ').split(' , ')
if len(x) < 3 or x[1][0] == 'м' and len(x) <= 3:
return False
if x[1][0] == 'м':
x = x[3]
else:
x = x[2]
x = x.split(' ')
for i in x:
if i == 'не':
return False
return True
hh_data['Готовность к командировкам'] = hh_data['Город, переезд, командировки'].apply(business_trips_f)
Сколько процентов соискателей живут в Санкт-Петербурге?
hh_data[hh_data['Город'] == 'Санкт-Петербург'].shape[0] / hh_data.shape[0] * 100
11.033881637761487
Сколько процентов соискателей готовы одновременно и к переездам, и к командировкам?
hh_data[(hh_data['Готовность к переезду'] == True) & (hh_data['Готовность к командировкам'] == True)].shape[0] / hh_data.shape[0] * 100
31.88136956910424
Удалим столбец "Город, переезд, командировки"
hh_data = hh_data.drop(['Город, переезд, командировки'], axis=1)
Такой вариант признаков имеет множество различных комбинаций, а значит множество уникальных значений, что мешает анализу.
Преобразуем категориальные признаки в One Hot Encoding
employments = ['полная занятость', 'частичная занятость',
'проектная работа', 'волонтерство', 'стажировка']
charts = ['полный день', 'сменный график',
'гибкий график', 'удаленная работа',
'вахтовый метод']
for employment, chart in zip(employments, charts):
hh_data[employment] = hh_data['Занятость'].apply(lambda x: employment in x)
hh_data[chart] = hh_data['График'].apply(lambda x: chart in x)
Сколько людей ищут проектную работу и волонтёрство?
hh_data[hh_data['проектная работа'] & hh_data['волонтерство']].shape[0]
436
Сколько людей хотят работать вахтовым методом и с гибким графиком?
hh_data[hh_data['вахтовый метод'] & hh_data['гибкий график']].shape[0]
2311
Удалим столбцы «Занятость» и «График»
hh_data = hh_data.drop(['Занятость','График'], axis=1)
Сделаем выгрузку курсов валют, которые встречаются в наших данных за период с 29.12.2017 по 05.12.2019. И сохраним в csv файл.
Создайте новый DataFrame из полученного файла. В полученной таблице будут столбцы:
В признаке "Обновление резюме", содержится дата и время, когда соискатель выложил текущий вариант своего резюме. По дате и будем сопоставлять курсы валют.
Считаем данные курса из файла
exch_data = pd.read_csv('ExchangeRates.csv', sep=',')
Переведём признак "Обновление резюме" из таблицы с резюме в формат datetime и создадим столбец с датой. В тот же формат приведем признак "date" из таблицы с валютами.
hh_data['date'] = pd.to_datetime(hh_data['Обновление резюме'], dayfirst=True).dt.date
exch_data['date'] = pd.to_datetime(exch_data['date'], dayfirst=True).dt.date
Выделим из столбца "ЗП" сумму желаемой заработной платы и наименование валюты, в которой она исчисляется. Наименование валюты переведем в стандарт ISO.
def get_salary_sum(x): # получаем зп
return float(x.split(' ')[0])
def get_salary_currency(arg): # получаем курс
currency_dict = {
'USD': 'USD', 'KZT': 'KZT',
'грн': 'UAH', 'белруб': 'BYN',
'EUR': 'EUR', 'KGS': 'KGS',
'сум': 'UZS', 'AZN': 'AZN',
'руб': 'RUB'
}
curr = arg.split(' ')[1].replace('.', '')
return currency_dict[curr]
hh_data['ЗП (tmp)'] = hh_data['ЗП'].apply(get_salary_sum)
hh_data['Курс (tmp)'] = hh_data['ЗП'].apply(get_salary_currency)
Присоединим к таблице с резюме таблицу с курсами по столбцам с датой и названием валюты. Значение close для рубля заполним единицей (курс рубля самого к себе)
merged = hh_data.merge(
exch_data,
left_on=['Курс (tmp)', 'Обновление резюме'],
right_on=['currency', 'date',],
how='left'
)
merged['close'] = merged['close'].fillna(1) # заполним пропуски
merged['proportion'] = merged['proportion'].fillna(1)
Умножим сумму желаемой заработной платы на присоединенный курс валюты (close) и разделить на пропорцию.
hh_data['ЗП (руб)'] = merged['close'] * merged['ЗП (tmp)'] / merged['proportion']
Чему равна желаемая медианная заработная плата соискателей?
hh_data['ЗП (руб)'].median()/1000
60.0
Удалим исходный столбец заработной платы "ЗП" и все промежуточные столбцы.
hh_data = hh_data.drop(['ЗП', 'ЗП (tmp)', 'Курс (tmp)','date'], axis=1)
import plotly.express as px
import plotly.graph_objs as go
fig = px.histogram(
data_frame=hh_data,
x='Возраст',
title='Распределение Возраст соискателей',
width=500,
marginal='box',
)
fig.show()
Вывод:
fig = px.histogram(
data_frame=hh_data,
x='Опыт работы (месяц)',
title='Распределение опыта работы соискателей',
width=500,
marginal='box',
)
fig.show()
Вывод:
fig = px.histogram(
data_frame=hh_data,
x='ЗП (руб)',
title='Распределение желаемой з/п соискателей',
width=500,
marginal='box'
)
fig.show()
Вывод:
temp_data = hh_data[hh_data['ЗП (руб)']<1e6].groupby('Образование', as_index=False).median()
fig = px.bar(
data_frame=temp_data,
x='Образование',
y='ЗП (руб)',
title='Медианная з/п по уровню образования'
)
fig.show()
C:\Users\rustem\AppData\Local\Temp\ipykernel_13520\2951648698.py:1: FutureWarning: The default value of numeric_only in DataFrameGroupBy.median is deprecated. In a future version, numeric_only will default to False. Either specify numeric_only or select only columns which should be valid for the function.
Вывод: Можно сделать вывод, что от образования зависит зп, для высшего оно наибольшее, для среднего и среднего специального зп наименьшее
temp_data = hh_data[hh_data['ЗП (руб)']<1e6]
fig = px.box(
data_frame=temp_data,
x='Город',
y='ЗП (руб)',
title='Распределение з/п по городам'
)
fig.show()
Вывод: Можно заметить, что зп зависит от города, в городах-миллионниках зп меньше, по сравнению с Санкт-Петербургом и Москвой. В "Других" зп наибольшая, так как в этой категории все остальные города России.
temp_data = hh_data.groupby(
['Готовность к командировкам', 'Готовность к переезду'],
as_index=False
)['ЗП (руб)'].median()
fig = px.bar(
data_frame=temp_data,
y='Готовность к переезду',
x='ЗП (руб)',
barmode="group",
color='Готовность к командировкам',
title='Медианная з/п по готовности к командировкам/переезду'
)
fig.show()
Вывод: Уровень зп зависит от возможности командировок и переездов - чем больше вариаций, тем больше зарплата
temp_data = hh_data.pivot_table(
index='Образование',
columns='Возраст',
values='ЗП (руб)',
aggfunc='median',
fill_value=0
)
fig = px.imshow(
temp_data,
aspect='auto',
color_continuous_scale='reds',
title='Медианная з/п по образованию и возрасту'
)
fig.show()
Вывод: Для высшего образования рост зп происходит быстрее всего и больше, чем в других категориях. Также неоконченное высшее цениться больше, чем среднее образование. Видим аномалию в зп - для высшего образования в возрасте 17 лет средняя зарплата 505т
Для большей наглядности можем убрать все аномалии - выберем зарплату, меньшую, чем 1000000 рублей
temp_data = hh_data[hh_data['ЗП (руб)'] < 1000000]
temp_data = temp_data.pivot_table(
index='Образование',
columns='Возраст',
values='ЗП (руб)',
aggfunc='median',
fill_value=0
)
fig = px.imshow(
temp_data,
aspect='auto',
color_continuous_scale='reds',
title='Медианная з/п по образованию и возрасту',
)
fig.show()
x = [0, 100]
import seaborn as sns
temp_data = hh_data.copy()
temp_data['Опыт работы (год)'] = temp_data['Опыт работы (месяц)']/12
fig = px.scatter(
temp_data,
x='Возраст',
y='Опыт работы (год)',
title = 'Зависимость опыта работы от возраста')
fig.add_trace(go.Scatter(x=x, y=x))
fig.show();
Вывод: Из нашего графика можно сделать вывод, что в БД присутсвтуют 7 аномалий, связанные с оптытом работы
temp_data = hh_data[hh_data['ЗП (руб)']<1e6]
fig = px.box(
data_frame=temp_data,
x='Пол',
y='ЗП (руб)',
title='Распределение з/п по полам'
)
fig.show()
Вывод: Можно заметить, что мужчины в среднем зарабатывают больше, чем женщины, также зарплата у мужчин имеют больший размах
temp_data = hh_data[hh_data['стажировка'] == True]
fig = px.histogram(
data_frame=hh_data,
x='Возраст',
title='Распределение стажировок по возрастам',
width=500,
marginal='box'
)
fig.show()
Вывод: С повышением возраста, кол-во людей, которые ищут стажировку уменьшаются
duplicates = hh_data[hh_data.duplicated(subset=hh_data.columns)]
hh_data = hh_data.drop_duplicates()
duplicates.shape[0]
158
hh_data.isnull().sum()
Ищет работу на должность: 0 Последнее/нынешнее место работы 1 Последняя/нынешняя должность 2 Обновление резюме 0 Авто 0 Образование 0 Пол 0 Возраст 0 Опыт работы (месяц) 168 Город 0 Готовность к переезду 0 Готовность к командировкам 0 полная занятость 0 полный день 0 частичная занятость 0 сменный график 0 проектная работа 0 гибкий график 0 волонтерство 0 удаленная работа 0 стажировка 0 вахтовый метод 0 ЗП (руб) 0 dtype: int64
hh_data = hh_data.dropna(subset=['Последнее/нынешнее место работы', 'Последняя/нынешняя должность'])
hh_data['Опыт работы (месяц)'] = hh_data['Опыт работы (месяц)'].fillna(hh_data['Опыт работы (месяц)'].median())
delete_data = hh_data[(hh_data['ЗП (руб)'] > 1e6) | (hh_data['ЗП (руб)'] < 1e3)]
hh_data = hh_data.drop(delete_data.index)
print(f'Удалено {delete_data.shape[0]} записей')
Удалено 435 записей
delete_data = hh_data[hh_data['Опыт работы (месяц)']/12 >= hh_data['Возраст']]
hh_data = hh_data.drop(delete_data.index)
print(f'Удалено {delete_data.shape[0]} записей')
Удалено 7 записей
Найдём выбросы с помощью метода z-отклонения и удалим их из данных, используя логарифмический масштаб.
Выведим таблицу с полученными выбросами и оценим их.
fig, ax = plt.subplots(1, 1, figsize=(8, 4))
log_age = np.log(hh_data['Возраст'] + 1)
histplot = sns.histplot(log_age, bins=30, ax=ax)
histplot.axvline(log_age.mean(), color='k', lw=2)
histplot.axvline(log_age.mean()+ 4 *log_age.std(), color='k', ls='--', lw=2)
histplot.axvline(log_age.mean()- 3 *log_age.std(), color='k', ls='--', lw=2)
histplot.set_title('Log Age Distribution');
def outliers_z_score_mod(data, feature, left=3, right=4, log_scale=False):
if log_scale:
x = np.log(data[feature]+1)
else:
x = data[feature]
mu = x.mean()
sigma = x.std()
lower_bound = mu - left * sigma
upper_bound = mu + right * sigma
delete_data = data[(x < lower_bound) | (x > upper_bound)]
cleaned = data[(x >= lower_bound) & (x <= upper_bound)]
return delete_data, cleaned
delete_data, hh_data = outliers_z_score_mod(hh_data, 'Возраст', left=3, right=4, log_scale=True)
print(f'Удалено {delete_data.shape[0]} записей')
delete_data['Возраст']
Удалено 3 записей
31137 15 32950 15 33654 100 Name: Возраст, dtype: int64
Вывод: График асимметричен в левую сторону, под категорию выбросов попадают люди с возрастом 15, 15 и 100 лет